"""
    MINOR MODIFICATION FOR ClassySORT:

    In the original implementation of SORT, 
    it threw away the object classification category information 
    For example, (0: person, 1: bike, etc.)
    
    I needed to keep that information for use in `Watchout`, 
    so I added a `detclass` attribute to the `KalmanBoxTracker` object
    which stores YOLO detection object class information.
    With this modification, SORT returns data in the format:

    `[x_left_top, y_left_top, x_right_bottom, y_right_bottom, object_category, object_identification]`


    ==========================================================================

    
    SORT: A Simple, Online and Realtime Tracker
    Copyright (C) 2016-2020 Alex Bewley alex@bewley.ai

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
from __future__ import print_function

import os
import numpy as np
import matplotlib

matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import matplotlib.patches as patches  # 用于绘制常见图像（如矩形，椭圆，圆形，多边形）
from skimage import io

import glob
import time
import argparse
from filterpy.kalman import KalmanFilter  # filterpy包含了一些常用滤波器的库

np.random.seed(0)


def linear_assignment(cost_matrix):
    """
    匈牙利匹配计算出检测框与跟踪框能够匹配起来的索引对
    :param cost_matrix:
    :return:
    """
    try:
        import lap  # linear assignment problem solver
        _, x, y = lap.lapjv(cost_matrix, extend_cost=True)
        return np.array([[y[i], i] for i in x if i >= 0])
    except ImportError:
        from scipy.optimize import linear_sum_assignment
        x, y = linear_sum_assignment(cost_matrix)
        return np.array(list(zip(x, y)))


def iou_batch(bb_test, bb_gt):
    """ 通过计算检测框与跟踪框的IOU，得到 cost matrix
    From SORT: Computes IOU between two boxes in the form [x1,y1,x2,y2]
    """
    bb_gt = np.expand_dims(bb_gt, 0)
    bb_test = np.expand_dims(bb_test, 1)

    xx1 = np.maximum(bb_test[..., 0], bb_gt[..., 0])
    yy1 = np.maximum(bb_test[..., 1], bb_gt[..., 1])
    xx2 = np.minimum(bb_test[..., 2], bb_gt[..., 2])
    yy2 = np.minimum(bb_test[..., 3], bb_gt[..., 3])
    w = np.maximum(0., xx2 - xx1)
    h = np.maximum(0., yy2 - yy1)
    wh = w * h
    # IOU=（bb_test和bb_gt框相交部分面积）/(bb_test框面积+bb_gt框面积 - 两者相交面积)
    o = wh / ((bb_test[..., 2] - bb_test[..., 0]) * (bb_test[..., 3] - bb_test[..., 1])
              + (bb_gt[..., 2] - bb_gt[..., 0]) * (bb_gt[..., 3] - bb_gt[..., 1]) - wh)
    return (o)


# 将bbox由[x1,y1,x2,y2]形式转为 [框中心点x,框中心点y,框面积s,宽高比例r]^T
def convert_bbox_to_z(bbox):
    """
    Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form [x,y,s,r] where x,y is the center of the box and s is the scale/area and r is the aspect ratio
    """
    w = bbox[2] - bbox[0]
    h = bbox[3] - bbox[1]
    x = bbox[0] + w / 2.
    y = bbox[1] + h / 2.
    s = w * h  # scale is just area
    r = w / float(h)
    return np.array([x, y, s, r]).reshape((4, 1))  # 将数组转为4行一列形式，即[x,y,s,r]^T


def convert_x_to_bbox(x, score=None):
    """ 将[x,y,s,r]形式的bbox，转为[x1,y1,x2,y2]形式
    Takes a bounding box in the centre form [x,y,s,r] and returns it in the form
    [x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right
    :param score: 是否返回置信度
    """
    w = np.sqrt(x[2] * x[3])  # w=sqrt(w*h * w/h)
    h = x[2] / w              # h=w*h/w
    if (score == None):  # 如果检测框不带置信度
        return np.array([x[0] - w / 2., x[1] - h / 2., x[0] + w / 2., x[1] + h / 2.]).reshape((1, 4))  # 返回[x1,y1,x2,y2]
    else:  # 如果加测框带置信度
        return np.array([x[0] - w / 2., x[1] - h / 2., x[0] + w / 2., x[1] + h / 2., score]).reshape((1, 5))  # 返回[x1,y1,x2,y2,score]


class KalmanBoxTracker(object):
    """
    This class represents the internal state of individual tracked objects observed as bbox.  # 此类表示作为bbox观察到的单个跟踪对象的内部状态。
    """
    count = 0

    def __init__(self, bbox):
        """ 使用初始边界框初始化跟踪器
        Initialize a tracker using initial bounding box
        
        Parameter 'bbox' must have 'detected class' int number at the -1 position.
        """
        # define constant velocity model                # 定义匀速模型
        self.kf = KalmanFilter(dim_x=7, dim_z=4)  # 状态变量是7维， 观测值是4维的(self.kf.R)，按照需要的维度构建目标
        self.kf.F = np.array(
            [[1, 0, 0, 0, 1, 0, 0],
             [0, 1, 0, 0, 0, 1, 0],
             [0, 0, 1, 0, 0, 0, 1],
             [0, 0, 0, 1, 0, 0, 0],
             [0, 0, 0, 0, 1, 0, 0],
             [0, 0, 0, 0, 0, 1, 0],
             [0, 0, 0, 0, 0, 0, 1]])
        self.kf.H = np.array(
            [[1, 0, 0, 0, 0, 0, 0],
             [0, 1, 0, 0, 0, 0, 0],
             [0, 0, 1, 0, 0, 0, 0],
             [0, 0, 0, 1, 0, 0, 0]])

        self.kf.R[2:, 2:] *= 10.  # R: Covariance matrix of measurement noise (set to high for noisy inputs -> more 'inertia' of boxes')  # 测量噪声的协方差矩阵（噪声输入设置为高->框的“惯性”更多）
        self.kf.P[4:, 4:] *= 1000.  # give high uncertainty to the unobservable initial velocities  # 对未观测到的初始速度给出高的不确定性
        self.kf.P *= 10.   # 默认定义的协方差矩阵是np.eye(dim_x)，将P中的数值与10， 1000相乘，赋值不确定性
        self.kf.Q[-1, -1] *= 0.5  # Q: Covariance matrix of process noise (set to high for erratically moving things)  # 过程噪声的协方差矩阵（对于不规则移动的物体，设置为高）
        self.kf.Q[4:, 4:] *= 0.5

        self.kf.x[:4] = convert_bbox_to_z(bbox)  # STATE VECTOR  # 将bbox转为 [x,y,s,r]^T形式，赋给状态变量X的前4位
        self.time_since_update = 0  # TODO:??? 越来越迷了, 这是啥意思呢?
        print("now: at Kalm->_init_(), self.time_since_update=%s" % self.time_since_update)

        self.id = KalmanBoxTracker.count  # 当前建立的卡尔曼对象的ID
        KalmanBoxTracker.count += 1  # 总的卡尔曼跟踪的个数
        self.history = []
        self.hits = 0
        self.hit_streak = 0  # 判断当前是否做了更新，大于等于1的说明做了更新，只要连续帧中没有做连续更新，hit_streak就会清零
        self.age = 0

        # keep yolov5 detected class information
        self.detclass = bbox[5]  # 将检测到的类别赋值给 detclass

    def update(self, bbox):
        """使用观察到的bbox更新状态向量, 用检测框替换跟踪器self.trackers列表中对应的跟踪框
        Updates the state vector with observed bbox
        """
        self.time_since_update = 0  #TODO: ???
        print("now: at Kalm->update(), self.time_since_update=%s" % self.time_since_update)
        self.history = []
        self.hits += 1
        self.hit_streak += 1  # 判断当前是否做了更新，大于等于1的说明做了更新，只要连续帧中没有做连续更新，hit_streak就会清零
        self.kf.update(convert_bbox_to_z(bbox))
        self.detclass = bbox[5]

    def predict(self):
        """ 推进状态向量并返回预测的边界框估计值, 也就是:卡尔曼滤波对跟踪器列表中的目标进行下一时刻位置的预测
        Advances the state vector and returns the predicted bounding box estimate
        """
        if ((self.kf.x[6] + self.kf.x[2]) <= 0):  # TODO:???
            self.kf.x[6] *= 0.0
        self.kf.predict()
        self.age += 1  # TODO:???
        if (self.time_since_update > 0):  # 如果连续 [x]+1 帧都没有检测框的话，那么就丢失跟踪, 这里更改跟踪的帧数，默认只跟踪[0+1=]1帧
            self.hit_streak = 0  # 判断当前是否做了更新，大于等于1的说明做了更新，只要连续帧中没有做连续更新，hit_streak就会清零
        self.time_since_update += 1  # TODO:???
        print("now: at Kalm->predict(), self.time_since_update=%s" % self.time_since_update)
        self.history.append(convert_x_to_bbox(self.kf.x))
        return self.history[-1]

    def get_state(self):
        """ 返回当前边界框估计值
        Returns the current bounding box estimate
        # test
        arr1 = np.array([[1,2,3,4]])
        arr2 = np.array([0])
        arr3 = np.expand_dims(arr2, 0)
        np.concatenate((arr1,arr3), axis=1)
        """
        arr_detclass = np.expand_dims(np.array([self.detclass]), 0)

        arr_u_dot = np.expand_dims(self.kf.x[4], 0)  # 扩充维度是为了数据对齐  # x_中心的时间导数
        arr_v_dot = np.expand_dims(self.kf.x[5], 0)  # y_中心的时间导数
        arr_s_dot = np.expand_dims(self.kf.x[6], 0)  # 标度（bbox面积）的时间导数，以像素为单位

        return np.concatenate((convert_x_to_bbox(self.kf.x), arr_detclass, arr_u_dot, arr_v_dot, arr_s_dot), axis=1)


def associate_detections_to_trackers(detections, trackers, iou_threshold=0.3):
    """ 用于将检测与跟踪进行关联, 将物体检测的BBox与卡尔曼滤波器预测的跟踪BBox匹配
    Assigns detections to tracked object (both represented as bounding boxes)
    Returns 3 lists of 
    1. matches,  # 匹配关联, 类型???
    2. unmatched_detections  # 未匹配的 det
    3. unmatched_trackers  # 未匹配的 跟踪器
    """
    # 第一帧没有跟踪框，只有检测框，所以返回3个值：（1）匹配到的[d,t]（空的）；（2）没有匹配到的检测框；（3）没有匹配到的跟踪框（空的）
    if (len(trackers) == 0):  # 如果跟踪器为空
        return np.empty((0, 2), dtype=int), np.arange(len(detections)), np.empty((0, 5), dtype=int)

    iou_matrix = iou_batch(detections, trackers)  # 检测器与跟踪器IOU矩阵

    if min(iou_matrix.shape) > 0:  # 匹配到了跟踪框
        a = (iou_matrix > iou_threshold).astype(np.int32)  # 阈值大于设定阈值的匹配框
        if a.sum(1).max() == 1 and a.sum(0).max() == 1:  # TODO: 可能是每行每列都是1个, 意味着就是唯一符合条件的
            matched_indices = np.stack(np.where(a), axis=1)
        else:
            # 参考：https://blog.csdn.net/herr_kun/article/details/86509591
            # 加上负号是因为linear_assignment求的是最小代价组合，而我们需要的是IOU最大的组合方式，所以取负号
            # 通过匈牙利算法匹配卡尔曼滤波器预测的BBox与物体检测BBox以[[d,t],,,]的二维矩阵保存到 matched_indices
            matched_indices = linear_assignment(-iou_matrix)  # 匈牙利匹配计算出检测框与跟踪框能够匹配起来的索引对
    else:
        matched_indices = np.empty(shape=(0, 2))

    # 没有匹配上的物体检测BBox放入unmatched_detections列表，表示有新的物体进入画面了，后面要新增跟踪器追踪新物体
    unmatched_detections = []  # 未匹配上的检测器, 就是当前帧中检测的物体多了
    for d, det in enumerate(detections):
        if (d not in matched_indices[:, 0]):  # 如果检测器中第d个检测结果不在匹配结果索引中，则d未匹配上
            unmatched_detections.append(d)

    # 没有匹配上的卡尔曼滤波器预测的BBox放入unmatched_trackers列表，表示之前跟踪的物体离开画面了，后面要删除对应的跟踪器
    unmatched_trackers = []  # 未匹配上的跟踪器
    for t, trk in enumerate(trackers):
        if (t not in matched_indices[:, 1]):  # 如果跟踪器中第t个跟踪结果不在匹配结果索引中，则t未匹配上
            unmatched_trackers.append(t)

    # 遍历matched_indices矩阵，将IOU值小于iou_threshold的匹配结果分别放入unmatched_detections，unmatched_trackers列表中
    # filter out matched with low IOU  # 过滤掉那些IOU较小的匹配对
    matches = []  # 存放过滤后的匹配结果
    for m in matched_indices:  # 遍历粗匹配结果, 小于设定的阈值则认为不匹配
        if (iou_matrix[m[0], m[1]] < iou_threshold):  # m[0]是检测器ID， m[1]是跟踪器ID，如它们的IOU小于阈值则将它们视为未匹配成功
            unmatched_detections.append(m[0])
            unmatched_trackers.append(m[1])
        else:
            matches.append(m.reshape(1, 2))  # 将过滤后的匹配对维度变形成1x2形式

    if (len(matches) == 0):  # 如果过滤后匹配结果为空，那么返回空的匹配结果  # 说明并没有匹配到...
        matches = np.empty((0, 2), dtype=int)
    else:  # 如果过滤后匹配结果非空，则按0轴方向继续添加匹配对
        matches = np.concatenate(matches, axis=0)  # 匹配上的卡尔曼滤波器预测的BBox与物体检测的BBox以[[d,t],,,]的形式放入matches矩阵

    return matches, np.array(unmatched_detections), np.array(unmatched_trackers)  # 其中跟踪器数组是5列的（最后一列是ID）


class Sort(object):
    def __init__(self, max_age=1, min_hits=3, iou_threshold=0.3):
        """
        Parameters for SORT
        :param max_age: 即使对象在n帧中被遮挡或未检测到，也要跟踪对象  (默认5, 防止跟踪对象丢失), 连续预测的最大次数，就是放在self.trackers跟踪器列表中的框用卡尔曼滤波器连续预测位置的最大次数
        :param min_hits: min_hits：最小更新的次数，就是放在self.trackers跟踪器列表中的框与检测框匹配上，
        然后调用卡尔曼滤波器类中的update函数的最小次数，
        min_hits不设置为0是因为第一次检测到的目标不用跟踪，只需要加入到跟踪器列表中，不会显示，
        这个值不能设大，一般就是1，表示如果连续两帧都检测到目标，
        :param iou_threshold: 关联的两个帧之间的联合阈值的交集  (默认0.2)
        """
        self.max_age = max_age  # 连续预测的最大次数，就是放在self.trackers跟踪器列表中的框用卡尔曼滤波器连续预测位置的最大次数
        self.min_hits = min_hits
        self.iou_threshold = iou_threshold
        self.trackers = []  # 用来存储各个跟踪的卡尔曼对象
        self.frame_count = 0

    def update(self, dets=np.empty((0, 6))):  # 输入的是检测结果[x1,y1,x2,y2,score]形式
        """
        Parameters:
        'dets' - a numpy array of detection in the format [[x1, y1, x2, y2, score], [x1,y1,x2,y2,score],...]
        “dets”-格式为[[x1，y1，x2，y2，score]，[x1，y1，x2，y2，score]，…]的检测numpy数组
        
        Ensure to call this method even frame has no detections. (pass np.empty((0,5)))
        确保调用此方法，即使帧没有检测。（通过 np.empty（（0,5）） ）
        每一帧都得调用一次，即便检测结果为空
        
        Returns a similar array, where the last column is object ID (replacing confidence score)
        返回一个类似的数组，其中最后一列是对象ID（替换置信度得分）
        返回相似的数组，最后一列是目标ID
        
        NOTE: The number of objects returned may differ from the number of objects provided.
        注意：返回的对象数可能与提供的对象数不同。
        返回的目标数量可能与提供的检测数量不同
        """
        self.frame_count += 1  # 帧计数

        # Get predicted locations from existing trackers
        # 从现有跟踪器获取预测位置  # TODO: [xyxy, ?,?]
        # 根据当前所有的卡尔曼跟踪器的个数（等于上一帧中被跟踪的物体个数）创建二维矩阵，trks行号为卡尔曼跟踪器标识，列向量为跟踪BBox与物体跟踪ID
        trks = np.zeros((len(self.trackers), 6))  # 根据当前所有卡尔曼跟踪器的个数创建二维零矩阵，维度为：卡尔曼跟踪器ID个数x 6 (这6列内容为bbox与ID)
        to_del = []  # 存放待删除
        # ret：跟踪结果列表，返回形式：[[x1,y1,x2,y2,id1],,,[,,,]]，id表示多个目标的编号，以进行多个目标的区分
        ret = []  # 存放最后返回的结果  # 1*9 [xyxy,类别,X,X,X,当前跟踪对象的身份ID]
        for t, trk in enumerate(trks):  # 循环遍历卡尔曼跟踪器列表
            pos = self.trackers[t].predict()[0]  # 用卡尔曼跟踪器t产生对应物体在当前帧中预测的BBox
            trk[:] = [pos[0], pos[1], pos[2], pos[3], 0, 0]  # TODO: ??  这两个 0 是干啥的呢?
            if np.any(np.isnan(pos)):  # 如果预测的bbox为空，那么将第t个卡尔曼跟踪器删除
                to_del.append(t)
        # trks中存放了上一帧中被跟踪的所有物体在当前帧中预测的BBox
        trks = np.ma.compress_rows(np.ma.masked_invalid(trks))  # 把数组arr1中的无效值设置为masked(用--表示) # 将预测为空的卡尔曼跟踪器所在行删除，最后trks中存放的是上一帧中被跟踪的所有物体在当前帧中预测的非空bbox
        for t in reversed(to_del):  # 对to_del数组进行倒序遍历
            self.trackers.pop(t)  # 从跟踪器中删除 to_del中的上一帧跟踪器ID
        # 对传入的检测结果 与 上一帧跟踪物体在当前帧中预测的结果做关联，返回匹配的目标矩阵matched,
        # 新增目标的矩阵unmatched_dets, 离开画面的目标矩阵unmatched_trks
        # 将物体检测的BBox与卡尔曼滤波器预测的跟踪BBox匹配,获得跟踪成功的物体矩阵，新增物体的矩阵，离开画面的物体矩阵  # todo: 离开画面???
        matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets, trks, self.iou_threshold)

        # Update matched trackers with assigned detections # 使用指定的检测更新匹配的跟踪器
        # 对卡尔曼跟踪器做遍历
        # 如果上一帧中的t还在当前帧画面中（即不在当前预测的离开画面的矩阵unmatched_trks中）
        # 说明卡尔曼跟踪器t是关联成功的，在matched矩阵中找到与其关联的检测器d
        # 用关联的检测结果d来更新卡尔曼跟踪器（即用后验来更新先验）
        for m in matched:
            # 跟踪成功的物体BBox信息更新到对应的卡尔曼滤波器
            self.trackers[m[1]].update(dets[m[0], :])  # 用关联的检测结果d来更新卡尔曼跟踪器（即用后验来更新先验）

        # Create and initialize new trackers for unmatched detections # 对于新增的未匹配的检测结果，创建并初始化跟踪器
        for i in unmatched_dets:  # 新增目标  # 新增的物体要创建新的卡尔曼滤波器对象用于跟踪
            trk = KalmanBoxTracker(np.hstack((dets[i, :], np.array([0]))))  # 将新增的未匹配的检测结果dets[i,:]传入KalmanBoxTracker
            # trk = KalmanBoxTracker(np.hstack(dets[i,:])
            self.trackers.append(trk)  # 将新创建和初始化的跟踪器trk 传入trackers

        i = len(self.trackers)  # 目前一共存储了多少个 跟踪对象, 而非全部对象
        # 循环遍历所有的卡尔曼对象, 将跟踪成功的物体BBox与ID放入ret列表中
        for trk in reversed(self.trackers):  # 对新的卡尔曼跟踪器集进行倒序遍历
            d = trk.get_state()[0]  # 获取trk跟踪器的状态 [x1,y1,x2,y2, arr_detclass, arr_u_dot, arr_v_dot, arr_s_dot]
            # (trk.time_since_update < 1): 这个好像仅仅意味着就当前帧没检测到, 改为 2 试一下;
            # (trk.time_since_update < 1)表示连续预测的次数,self.min_hits不设置为0是因为第一次检测到的目标不用跟踪，不能设大，
            # min_hits：最小更新的次数，就是放在self.trackers跟踪器列表中的框与检测框匹配上，
            # 然后调用卡尔曼滤波器类中的update函数的最小次数，
            # min_hits不设置为0是因为第一次检测到的目标不用跟踪，只需要加入到跟踪器列表中，不会显示，
            # 这个值不能设大，一般就是1，表示如果连续两帧都检测到目标，
            # self.min_hits为最小更新的次数
            # hit_streak：判断当前是否做了更新，大于等于1的说明做了更新，只要连续帧中没有做连续更新，hit_streak就会清零
            if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):
                ret.append(np.concatenate((d, [trk.id + 1])).reshape(1, -1))  # +1'd because MOT benchmark requires positive value  # +1 d，因为MOT基准要求正值
            else:
                pass
                # ret.append(np.concatenate((d, [trk.id + 1])).reshape(1, -1))  # +1'd because MOT benchmark requires positive value  # +1 d，因为MOT基准要求正值
            i -= 1
            # remove dead tracklet  # 移除 逝去 tracklet
            # max_age：连续预测的最大次数，就是放在self.trackers跟踪器列表中的框用卡尔曼滤波器连续预测位置的最大次数
            if (trk.time_since_update > self.max_age):  # 即使对象在n帧中被遮挡或未检测到，也要跟踪对象  (默认5, 防止跟踪对象丢失)  # self.max_age为连续预测的最大次数
                self.trackers.pop(i)  # 移除第 i 个检测器
        if (len(ret) > 0):  # if 存在跟踪的对象
            # 返回当前画面中所有被跟踪物体的BBox与ID，二维矩阵[[x1,y1,x2,y2,id1],,,[,,,]],在main函数的帧循环中将当前帧的跟踪结果绘制和显示到屏幕上
            return np.concatenate(ret)
        return np.empty((0, 6))  # 否则返回一个空数组


def parse_args():
    """Parse input arguments."""
    parser = argparse.ArgumentParser(description='SORT demo')
    parser.add_argument('--display', dest='display', help='Display online tracker output (slow) [False]',
                        action='store_true')
    parser.add_argument("--seq_path", help="Path to detections.", type=str, default='data')
    parser.add_argument("--phase", help="Subdirectory in seq_path.", type=str, default='train')
    # max_age：连续预测的最大次数，就是放在self.trackers跟踪器列表中的框用卡尔曼滤波器连续预测位置的最大次数
    parser.add_argument("--max_age",
                        help="Maximum number of frames to keep alive a track without associated detections.",
                        type=int, default=1)
    # 4、min_hits：最小更新的次数，就是放在self.trackers跟踪器列表中的框与检测框匹配上，然后调用卡尔曼滤波器类中的update函数的最小次数，min_hits不设置为0是因为第一次检测到的目标不用跟踪，只需要加入到跟踪器列表中，不会显示，这个值不能设大，一般就是1，表示如果连续两帧都检测到目标，
    parser.add_argument("--min_hits",
                        help="Minimum number of associated detections before track is initialised.",
                        type=int, default=3)
    parser.add_argument("--iou_threshold", help="Minimum IOU for match.", type=float, default=0.3)
    args = parser.parse_args()
    return args


if __name__ == '__main__':
    # all train
    args = parse_args()
    display = args.display
    phase = args.phase
    total_time = 0.0
    total_frames = 0
    colours = np.random.rand(32, 3)  # used only for display
    if (display):
        if not os.path.exists('mot_benchmark'):
            print(
                '\n\tERROR: mot_benchmark link not found!\n\n    Create a symbolic link to the MOT benchmark\n    (https://motchallenge.net/data/2D_MOT_2015/#download). E.g.:\n\n    $ ln -s /path/to/MOT2015_challenge/2DMOT2015 mot_benchmark\n\n')
        exit()
    plt.ion()  # 用于动态绘制显示图像
    fig = plt.figure()
    ax1 = fig.add_subplot(111, aspect='equal')

    if not os.path.exists('output'):
        os.makedirs('output')
    pattern = os.path.join(args.seq_path, phase, '*', 'det', 'det.txt')
    for seq_dets_fn in glob.glob(pattern):
        # max_age：连续预测的最大次数，就是放在self.trackers跟踪器列表中的框用卡尔曼滤波器连续预测位置的最大次数
        # min_hits：最小更新的次数，就是放在self.trackers跟踪器列表中的框与检测框匹配上，
        # 然后调用卡尔曼滤波器类中的update函数的最小次数，
        # min_hits不设置为0是因为第一次检测到的目标不用跟踪，只需要加入到跟踪器列表中，不会显示，
        # 这个值不能设大，一般就是1，表示如果连续两帧都检测到目标，
        mot_tracker = Sort(max_age=args.max_age,
                           min_hits=args.min_hits,
                           iou_threshold=args.iou_threshold)  # create instance of the SORT tracker  #  创建Sort 跟踪实例
    seq_dets = np.loadtxt(seq_dets_fn, delimiter=',')  # 加载检测结果
    seq = seq_dets_fn[pattern.find('*'):].split(os.path.sep)[0]

    with open(os.path.join('output', '%s.txt' % (seq)), 'w') as out_file:
        print("Processing %s." % (seq))
        for frame in range(int(seq_dets[:, 0].max())):  # 确定视频序列总帧数，并进行for循环
            frame += 1  # detection and frame numbers begin at 1  # 由于视频序列帧数是从1开始的，因此加1
            dets = seq_dets[seq_dets[:, 0] == frame, 2:7]  # 提取检测结果中的[x1,y1,w,h,score]到dets
            dets[:, 2:4] += dets[:, 0:2]  # convert to [x1,y1,w,h] to [x1,y1,x2,y2]  # convert to [x1,y1,w,h] to [x1,y1,x2,y2]   将dets中的第2,3列的数加上第0,1列的数后赋值给2,3列；
            total_frames += 1  # 总帧数累计

        if (display):  # 如果要求显示结果
            fn = os.path.join('mot_benchmark', phase, seq, 'img1', '%06d.jpg' % (frame))  # 原图像路径名
            im = io.imread(fn)  # 加载图像
            ax1.imshow(im)  # 显示图像
            plt.title(seq + ' Tracked Targets')

        start_time = time.time()
        trackers = mot_tracker.update(dets)  # sort跟踪器更新
        cycle_time = time.time() - start_time  # sort跟踪器耗时
        total_time += cycle_time  # sort跟踪器总共耗费时间

        for d in trackers:
            print('%d,%d,%.2f,%.2f,%.2f,%.2f,1,-1,-1,-1' % (frame, d[4], d[0], d[1], d[2] - d[0], d[3] - d[1]),
                  file=out_file)
            if (display):  # 如果显示，将目标检测框画上
                d = d.astype(np.int32)
                ax1.add_patch(patches.Rectangle((d[0], d[1]), d[2] - d[0], d[3] - d[1], fill=False, lw=3,
                                                ec=colours[d[4] % 32, :]))

        if (display):
            fig.canvas.flush_events()
            plt.draw()
            ax1.cla()

    print("Total Tracking took: %.3f seconds for %d frames or %.1f FPS" % (
    total_time, total_frames, total_frames / total_time))

    if (display):
        print("Note: to get real runtime results run without the option: --display")
